package jxtend.processor.eclipse.visitor;

import java.util.Map;

import jxtend.processor.eclipse.visitor.handler.ASTNodeVisitorContext;
import jxtend.processor.eclipse.visitor.handler.impl.BindingTagBits;
import jxtend.processor.reflect.ReflectField;
import jxtend.processor.utils.ArrayUtils;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.ConstructorDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Literal;
import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.NameReference;
import org.eclipse.jdt.internal.compiler.ast.QualifiedAllocationExpression;
import org.eclipse.jdt.internal.compiler.ast.QualifiedNameReference;
import org.eclipse.jdt.internal.compiler.ast.SingleNameReference;
import org.eclipse.jdt.internal.compiler.ast.SwitchStatement;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.impl.Constant;
import org.eclipse.jdt.internal.compiler.lookup.AnnotationBinding;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ExtraCompilerModifiers;
import org.eclipse.jdt.internal.compiler.lookup.LocalTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodScope;
import org.eclipse.jdt.internal.compiler.lookup.NestedTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ParameterizedTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ProblemReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;

/**
 * Fixes issue #4 by clearing instance variables of a JDT 'Statement' subclass instances as well as restoring it
 * bits (inherited from ASTNode) to it state as it was before the type resolve.
 */
@SuppressWarnings("restriction")
public class UndoTypeResolveASTNodeVisitor implements ASTNodeVisitor {
	private static final ReflectField<SourceTypeBinding, MethodBinding[]> methodsOfSourceTypeBinding =
		ReflectField.getField(SourceTypeBinding.class, MethodBinding[].class, "methods");

	private static final ReflectField<Annotation, AnnotationBinding> compilerAnnotationOfAnnotation =
		ReflectField.getField(Annotation.class, AnnotationBinding.class, "compilerAnnotation");

	private static final ReflectField<SwitchStatement, int[]> constantsOfSwitchStatement =
			ReflectField.getField(SwitchStatement.class, int[].class, "constants");

	private static final ReflectField<QualifiedNameReference, int[]> otherDepthsOfQualifiedNameReference =
			ReflectField.getField(QualifiedNameReference.class, int[].class, "otherDepths");

	private final Map<ASTNode, Integer> statementBits;
	private final Map<ASTNode, Integer> methodBindingModifiers;
	private final boolean ignoreAnnotations;
	private final boolean leaveActualReceiverType;
	private final boolean undoTypResolveOfLocalDeclarationsOfMethodScope;

//	private final List<ASTNode> parentNodes = new ArrayList<ASTNode>();

	public UndoTypeResolveASTNodeVisitor(Map<ASTNode, Integer> statementBits,
			Map<ASTNode, Integer> methodBindingModifiers, boolean ignoreAnnotations, boolean leaveActualReceiverType,
			boolean undoTypResolveOfLocalDeclarationsOfMethodScope) {
		this.statementBits = statementBits;
		this.methodBindingModifiers = methodBindingModifiers;
		this.ignoreAnnotations = ignoreAnnotations;
		this.leaveActualReceiverType = leaveActualReceiverType;
		this.undoTypResolveOfLocalDeclarationsOfMethodScope = undoTypResolveOfLocalDeclarationsOfMethodScope;
	}

	@Override
	public boolean visit(ASTNodeVisitorContext context, ASTNode node, Scope scope) {
//		if (node instanceof QualifiedAllocationExpression) {
//			return false;
//		}
		
//		if ((node instanceof Annotation) && ignoreAnnotations) {
//			return false;
//		}

//		node.bits &= ~ASTNode.DidResolve;

		Integer originalBits = statementBits.get(node);

		if (node instanceof AbstractMethodDeclaration) {
			Integer modifiers = methodBindingModifiers.get(node);

			if ((modifiers != null) && (((AbstractMethodDeclaration) node).binding != null)) {
//				if ((node.bits & ASTNodeBits.JXTEND_GENERATED) != 0) {
//					int existingModifiers = ((AbstractMethodDeclaration) node).binding.modifiers;
//
//					if ((existingModifiers & ExtraCompilerModifiers.AccImplementing) != 0) {
//						modifiers |= ExtraCompilerModifiers.AccImplementing;
//					}
//					if ((existingModifiers & ExtraCompilerModifiers.AccOverriding) != 0) {
//						modifiers |= ExtraCompilerModifiers.AccOverriding;
//					}
//
//					 methodBindingModifiers.put(node, modifiers); 
//				}

				((AbstractMethodDeclaration) node).binding.modifiers = modifiers;
			}
		}
		
		//TODO: should be log a warning if no bits could be found?!?
		if (originalBits != null) {
			node.bits = originalBits;
		}

		if (node instanceof FieldDeclaration && ((FieldDeclaration) node).binding != null
				&& ((FieldDeclaration) node).binding.type instanceof ProblemReferenceBinding) {
//			((FieldDeclaration) node).binding = null;
			
			
			
//			((FieldDeclaration) node).binding.setConstant(null);
		}

		if (node instanceof FieldDeclaration && ((FieldDeclaration) node).binding != null
				&& ((FieldDeclaration) node).initialization != null
				&& ((FieldDeclaration) node).initialization.constant != null
				&& ((FieldDeclaration) node).initialization.constant != Constant.NotAConstant) {
			((FieldDeclaration) node).binding.setConstant(((FieldDeclaration) node).initialization.constant);
		}
		
		if (node instanceof Literal) {
			((Literal) node).computeConstant();
		}

		if (node instanceof Expression && !(node instanceof Annotation)) { // && !leaveActualReceiverType) {
//			((Expression) node).constant = Constant.NotAConstant; //null;

//
//			if (!(node instanceof Annotation)
//					&& !((node instanceof TypeReference) && context.getParentNode() instanceof Argument)) {
				((Expression) node).resolvedType = null;
//			}
		} else if (node instanceof SwitchStatement) {
			SwitchStatement switchStatement = (SwitchStatement) node;
			switchStatement.cases = null;
			switchStatement.caseCount = 0;
			switchStatement.defaultCase = null;
			constantsOfSwitchStatement.set(switchStatement, null);
		}

		if (!leaveActualReceiverType) {
			@SuppressWarnings("unchecked") ReflectField<ASTNode, TypeBinding> actualReceiverTypeOfASTNode =
				(ReflectField<ASTNode, TypeBinding>) ReflectField.findPublicField(
						node.getClass(), TypeBinding.class, "actualReceiverType");
	
			if ((actualReceiverTypeOfASTNode != null)) {
//	  				&& (!(node instanceof NameReference) || (node.bits & ASTNode.DepthSHIFT) != 0)) {
				actualReceiverTypeOfASTNode.set(node, null);
			}
//		} else if (node instanceof MessageSend && ((MessageSend) node).binding != null
//				&& ((MessageSend) node).binding.declaringClass != null) {
////			if (scope instanceof ClassScope) {
//				((MessageSend) node).actualReceiverType = ((MessageSend) node).binding.declaringClass;
////			} else {
////				assert scope instanceof BlockScope;
////
//////				((MessageSend) node).actualReceiverType = ((MessageSend) node).receiver.resolveType((BlockScope) scope);
////			}
		}

		if (node instanceof FieldDeclaration && ((FieldDeclaration) node).binding != null
				&& ((FieldDeclaration) node).binding.isStatic() && ((FieldDeclaration) node).binding.isFinal()) {
			node.bits &= ~ASTNode.HasBeenResolved;
		}

		if (node instanceof QualifiedAllocationExpression
				&& ((QualifiedAllocationExpression) node).anonymousType != null) {
			AbstractMethodDeclaration[] anonymousTypeMethods =
					((QualifiedAllocationExpression) node).anonymousType.methods;
			if (anonymousTypeMethods != null && anonymousTypeMethods.length != 0
					&& anonymousTypeMethods[0] instanceof ConstructorDeclaration
					&& (anonymousTypeMethods[0].bits & ASTNode.IsDefaultConstructor) != 0) {
				anonymousTypeMethods[0].selector = new char[0];
			}
		}

		if ((node instanceof SingleNameReference) && ((node.bits & ASTNode.RestrictiveFlagMASK) == Binding.LOCAL)) {
			node.bits |= Binding.FIELD;
		}
		if ((node instanceof NameReference) && (((node.bits & ASTNode.RestrictiveFlagMASK) == Binding.FIELD)
				|| ((node.bits & ASTNode.RestrictiveFlagMASK) == Binding.LOCAL))) {
			node.bits |= Binding.VARIABLE;
		}

//		@SuppressWarnings("unchecked") ReflectField<ASTNode, Scope> scopeField =
//			(ReflectField<ASTNode, Scope>) ReflectField.findPublicField(node.getClass(), Scope.class, "scope");		
//		if (scopeField != null) {
//			Scope scopeOfASTNode = scopeField.get(node);

			if (scope instanceof BlockScope && (!(scope instanceof MethodScope)
					|| (undoTypResolveOfLocalDeclarationsOfMethodScope && scope instanceof MethodScope))
					&& node instanceof LocalDeclaration) { // && !(node instanceof Argument)) {
//					&& !leaveActualReceiverType) {
//					&& (node instanceof Argument && !(((LocalDeclaration) node).binding instanceof AptSourceLocalVariableBinding))) {
				BlockScope blockScopeOfASTNode = (BlockScope) scope; //scopeOfASTNode;

				int foundAtIndex = -1;
				
//				blockScopeOfASTNode.localIndex = 0;
				for (int i = 0; i < blockScopeOfASTNode.locals.length && blockScopeOfASTNode.locals[i] != null; i++) {
					if (CharOperation.equals(((LocalDeclaration) node).name, blockScopeOfASTNode.locals[i].name)) {
						blockScopeOfASTNode.locals[i] = null;
						foundAtIndex = i;
						break;
					}
				}

				if (foundAtIndex != -1) {
//					blockScopeOfASTNode.locals[foundAtIndex] = null;
					if ((blockScopeOfASTNode.localIndex - 1) > foundAtIndex) {
						for (int i = foundAtIndex; i < blockScopeOfASTNode.localIndex - 1; i++) {
							blockScopeOfASTNode.locals[i] = blockScopeOfASTNode.locals[i + 1];
						}
						blockScopeOfASTNode.locals[blockScopeOfASTNode.localIndex - 1] = null;
//					} else {
//						blockScopeOfASTNode.locals[foundAtIndex] = null;
					}

//					blockScopeOfASTNode.locals = ArrayUtils.removeFromArray(
//							LocalVariableBinding.class, blockScopeOfASTNode.locals, foundAtIndex);
					blockScopeOfASTNode.localIndex -= 1;
				}
//			}
		}
//		if ((node instanceof Annotation) && (((Annotation) node).recipient != null)) {
//			Binding annotationRecipient = ((Annotation) node).recipient;
//			Field tagBitsField = ReflectUtils.findPublicField(annotationRecipient.getClass(), "tagBits");
//			long tagBits = (Long) ReflectUtils.fieldValue(tagBitsField, annotationRecipient);
//			
//			tagBits &= ~TagBits.AnnotationResolved;
//			
//			ReflectUtils.setFieldValue(tagBitsField, annotationRecipient, tagBits);
//		}

		if (node instanceof Annotation) {
			((Annotation) node).recipient = null;
		}

//		if (node instanceof AbstractMethodDeclaration) {
//			((AbstractMethodDeclaration) node).binding.tagBits &= ~TagBits.AnnotationResolved;
//		}

//		Annotation parentAnnotation = context.findParentNode(Annotation.class);
//		if (parentAnnotation != null && node instanceof NameReference) {
//			Field bindingField = ReflectUtils.findPublicField(node.getClass(), "binding");
//			if ((bindingField != null)) {// && !MethodBinding.class.isAssignableFrom(bindingField.getType())) {
//				ReflectUtils.setFieldValue(bindingField, node, null);
//			}
//		}

//		if (node instanceof QualifiedAllocationExpression) {
//			((AllocationExpression) node).binding = null;
//		}
		
		//Issue #3: in case 'node' is an 'Annotation', we need to clear its 'compilerAnnotation'
		if (node instanceof Annotation) {
			compilerAnnotationOfAnnotation.set((Annotation) node, null);
		}

//		if (node instanceof FieldDeclaration) {
//			if (((FieldDeclaration) node).binding != null) {
//				((FieldDeclaration) node).binding.modifiers |= ExtraCompilerModifiers.AccUnresolved;
//			}
////			((FieldDeclaration) node).binding = null;
//		}

		ASTNode parentNode = context.getParentNode();
	
		//TODO: found out why I have to "local" NON-anonymous types
		if ((node instanceof TypeDeclaration && !(parentNode instanceof TypeDeclaration)
					&& !(parentNode instanceof CompilationUnitDeclaration) && (node.bits & ASTNode.IsAnonymousType) != 0)
				|| (node instanceof QualifiedAllocationExpression
						&& ((QualifiedAllocationExpression) node).anonymousType != null)) {
			//clear the default constructor of anonymous types
			clearDefaultConstructor(node instanceof TypeDeclaration
						? (TypeDeclaration) node : ((QualifiedAllocationExpression) node).anonymousType);
		}

		if (node instanceof QualifiedAllocationExpression
				&& ((QualifiedAllocationExpression) node).anonymousType != null
				&& ((QualifiedAllocationExpression) node).anonymousType.binding instanceof LocalTypeBinding) {
			LocalTypeBinding localTypeBinding =
					(LocalTypeBinding) ((QualifiedAllocationExpression) node).anonymousType.binding;
			if (localTypeBinding.enclosingType instanceof SourceTypeBinding
					&& !(localTypeBinding.enclosingType instanceof NestedTypeBinding)) {
				TypeDeclaration typeDeclaration =
						((SourceTypeBinding) localTypeBinding.enclosingType).scope.referenceContext;
				if (TypeDeclaration.kind(typeDeclaration.modifiers) == TypeDeclaration.ENUM_DECL) {
					clearDefaultConstructor(((QualifiedAllocationExpression) node).anonymousType);					
				}
			}
		}
//				&& ((QualifiedAllocationExpression) node).anonymousType.binding.enclosingType() instanceof SourceTypeBinding
//				&& ((QualifiedAllocationExpression) node).anonymousType.binding.enclosingType().re
//				
//						((org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding) this.binding).enclosingType().referenceContext()
//				&& TypeDeclaration.kind(((QualifiedAllocationExpression) node).anonymousType.g
//				&& TypeDeclaration.kind(((TypeDeclaration) node).modifiers) == TypeDeclaration.ENUM_DECL) {
//			TypeDeclaration typeDeclaration = (TypeDeclaration) node;
//			if (typeDeclaration.fields != null) {
//				for (FieldDeclaration fieldDeclaration : typeDeclaration.fields) {
//					if (fieldDeclaration.getKind() != AbstractVariableDeclaration.ENUM_CONSTANT
//							|| !(fieldDeclaration.initialization instanceof QualifiedAllocationExpression)) {
//						continue;
//					}
//
//					clearDefaultConstructor(
//							((QualifiedAllocationExpression) fieldDeclaration.initialization).anonymousType);
//				}
//			}
//		}
		
//		parentNodes.add(node);

		if (leaveActualReceiverType && node instanceof MethodDeclaration && ((MethodDeclaration) node).binding != null
				&& (((MethodDeclaration) node).binding.returnType instanceof SourceTypeBinding
					|| ((MethodDeclaration) node).binding.returnType instanceof ParameterizedTypeBinding)) {
//			((MethodDeclaration) node).binding = null;


//			((MethodDeclaration) node).binding.returnType = null;
			
//			SourceTypeBinding sourceTypeBinding = (SourceTypeBinding) ((MethodDeclaration) node).binding.returnType;
////			sourceTypeBinding.fPackage
//			
//			Constructor<UnresolvedReferenceBinding> constructor =
//					ReflectUtils.getInaccessibleConstructor(UnresolvedReferenceBinding.class, char[][].class, PackageBinding.class);
//			try {
//				UnresolvedReferenceBinding unresolvedReferenceBinding =
//						constructor.newInstance(sourceTypeBinding.compoundName, sourceTypeBinding.fPackage);
//				((MethodDeclaration) node).binding.returnType = unresolvedReferenceBinding;
//			} catch (IllegalArgumentException e) {
//				throw new JxtendProcessorException(e);
//			} catch (InstantiationException e) {
//				throw new JxtendProcessorException(e);
//			} catch (IllegalAccessException e) {
//				throw new JxtendProcessorException(e);
//			} catch (InvocationTargetException e) {
//				throw new JxtendProcessorException(e);
//			}
		}
		
//		if (leaveActualReceiverType && node instanceof FieldDeclaration && ((FieldDeclaration) node).binding != null
//				&& (((FieldDeclaration) node).binding.type instanceof SourceTypeBinding
//					|| ((FieldDeclaration) node).binding.type instanceof ParameterizedTypeBinding)) {
//			((FieldDeclaration) node).binding = null;
//		}

		if (leaveActualReceiverType && node instanceof TypeDeclaration && ((TypeDeclaration) node).binding != null) {
			((TypeDeclaration) node).binding.tagBits |= BindingTagBits.ALREADY_PROCESSED;
			
			if (((TypeDeclaration) node).binding.superInterfaces == null) {
				((TypeDeclaration) node).binding.superInterfaces = Binding.NO_SUPERINTERFACES;
			}
		}

//		if (node instanceof QualifiedNameReference) {
//			((QualifiedNameReference) node).otherBindings = null;
//			otherDepthsOfQualifiedNameReference.set((QualifiedNameReference) node, null);
//		}
		
//		if (leaveActualReceiverType && node instanceof MemberValuePair && ((MemberValuePair) node).value != null
//				&& ((MemberValuePair) node).value instanceof NameReference) {
//			 ((MemberValuePair) node).binding = null;
//			 ((MemberValuePair) node).compilerElementPair = null;
//			((NameReference) ((MemberValuePair) node).value).binding = null;
//			((NameReference) ((MemberValuePair) node).value).actualReceiverType = null;
//			((NameReference) ((MemberValuePair) node).value).resolvedType = null;
//		}

//		if (leaveActualReceiverType && node instanceof Annotation
//				&& ((Annotation) node).resolvedType instanceof SourceTypeBinding) {
////			((SourceTypeBinding) ((Annotation) node).resolvedType).setMethods(Binding.UNINITIALIZED_METHODS);
//
//			for (MethodBinding methodBinding : ((SourceTypeBinding) ((Annotation) node).resolvedType).methods()) {
//				if (!(methodBinding.returnType instanceof SourceTypeBinding)) {
//					continue;
//				}
//
//				SourceTypeBinding sourceTypeBinding = (SourceTypeBinding) methodBinding.returnType;
//				
//				Constructor<UnresolvedReferenceBinding> constructor =
//				ReflectUtils.getInaccessibleConstructor(UnresolvedReferenceBinding.class, char[][].class, PackageBinding.class);
//				try {
//					char[][] compoundName = sourceTypeBinding.compoundName;
//					if (CharOperation.contains('$', compoundName[compoundName.length - 1])) {
//						char[][] lastParts = CharOperation.splitOn('$', compoundName[compoundName.length - 1]);
//
////						compoundName = new char[][] { lastParts[lastParts.length - 1] };
//						char[][] newCompoundName = new char[compoundName.length - 1 + lastParts.length][]; // - 1 + lastParts.length
//						System.arraycopy(compoundName, 0, newCompoundName, 0, compoundName.length - 1);
////						newCompoundName[compoundName.length - 1] = lastParts[0];
//						for (int i = 0; i < lastParts.length; i++) {
//							newCompoundName[compoundName.length - 1 + i] = lastParts[i];
//						}
//						compoundName = newCompoundName;
//					}
//					
//					UnresolvedReferenceBinding unresolvedReferenceBinding =
//							constructor.newInstance(compoundName, sourceTypeBinding.fPackage);
//					methodBinding.returnType = unresolvedReferenceBinding;
//				} catch (IllegalArgumentException e) {
//					throw new JxtendProcessorException(e);
//				} catch (InstantiationException e) {
//					throw new JxtendProcessorException(e);
//				} catch (IllegalAccessException e) {
//					throw new JxtendProcessorException(e);
//				} catch (InvocationTargetException e) {
//					throw new JxtendProcessorException(e);
//				}
//
//			}
////			((TypeReference) node).resolvedType = null;
//		}

//		if (leaveActualReceiverType && node instanceof TypeDeclaration) {
//			((TypeDeclaration) node).binding = null;
//		}

		return true;
	}
 
	@Override
	public void endVisit(ASTNodeVisitorContext context, ASTNode node, Scope scope) {
//		parentNodes.remove(node);
	}

//	private ASTNode getParentNode() {
//		return !parentNodes.isEmpty() ? parentNodes.get(parentNodes.size() - 1) : null;
//	}

	public static void clearDefaultConstructor(TypeDeclaration typeDeclaration) {
		AbstractMethodDeclaration[] oldMethods = typeDeclaration.methods;
		int oldMethodsLength = (oldMethods != null) ? oldMethods.length : 0;

		if (oldMethodsLength == 0) { return; }

		int defaultConstructorIndex = -1;
		for (int i = 0; i < oldMethods.length; i++) {
			if (!(oldMethods[i] instanceof ConstructorDeclaration)) { continue; }

			ConstructorDeclaration constructorDeclaration = (ConstructorDeclaration) oldMethods[i];

			if ((constructorDeclaration.bits & ASTNode.IsDefaultConstructor) != 0) {
				defaultConstructorIndex = i;
				break;
			}
		}

		if (defaultConstructorIndex == -1) { return; }

		AbstractMethodDeclaration[] newMethods = ArrayUtils.removeFromArray(
				AbstractMethodDeclaration.class, oldMethods, defaultConstructorIndex);
		typeDeclaration.methods = newMethods;

		if (typeDeclaration.binding == null) { return; }

		MethodBinding[] methodBindings = typeDeclaration.binding.methods();
		if (methodBindings == null) { return; }

		int methodBindingIndex = -1;	
		for (int i = 0; i < methodBindings.length; i++) {
			if ((methodBindings[i].modifiers & ExtraCompilerModifiers.AccIsDefaultConstructor) != 0) {
				methodBindingIndex = i;
				break;
			}
		}

		if (methodBindingIndex == -1) { return; }

		MethodBinding[] newMethodBindings =
			ArrayUtils.removeFromArray(MethodBinding.class, methodBindings, methodBindingIndex);
		if (newMethodBindings.length > 1) {
			ReferenceBinding.sortMethods(newMethodBindings, 0, newMethodBindings.length);
		}

		methodsOfSourceTypeBinding.set(typeDeclaration.binding, newMethodBindings);
	}
}